{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 购物平台客服对话摘要\n",
    "\n",
    "<span style=\"font-size: 20px; font-weight: bold;\">注意：您使用该案例默认的数据和模型训练时，会产生一定费用。计费方式参考：https://cloud.baidu.com/doc/WENXINWORKSHOP/s/6lrk4bgxb</span>\n",
    "\n",
    "在购物平台上，顾客通常会咨询平台的商品。比如咨询商品的用途、价格，让平台推荐商品等。对于平台来说，有没有服务好顾客，有没有为顾客带来良好的购物体验也是能否留住客户、吸引客户的关键。所以，作为平台需要对平台的顾客与客服的对话内容进行分析，了解顾客咨询的问题、顾客对客服提供的方案是否满意、顾客有没有接受客服提供的方案、顾客的情绪等。进而对顾客数据进一步分析，制定提升顾客体验的策略。\n",
    "\n",
    "分析顾客的对话数据是比较耗时的工作，我们可以考虑调用大模型，使用大模型的生成能力整理对话数据，用于进一步的分析。\n",
    "\n",
    "千帆SDK为用户提供了大模型调用，训练，推理服务，本文将带您使用SDK的能力进行客服摘要工作。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 0. 环境准备\n",
    "\n",
    "引入必要的库\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [],
   "source": [
    "from qianfan import ChatCompletion\n",
    "from qianfan.dataset import Dataset\n",
    "from qianfan.common import Prompt\n",
    "from qianfan.trainer import LLMFinetune\n",
    "from qianfan.trainer.consts import PeftType\n",
    "from qianfan.trainer.configs import TrainConfig\n",
    "import os\n",
    "from qianfan.dataset import Dataset\n",
    "from qianfan.dataset.data_source import BosDataSource\n",
    "from qianfan.dataset.data_source.base import FormatType"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [],
   "source": [
    "os.environ[\"QIANFAN_ACCESS_KEY\"] = \"your_access_key\"\n",
    "os.environ[\"QIANFAN_SECRET_KEY\"] = \"your_secret_key\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 1. 基座模型效果示例"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "首先，我们选择了ERNIE-Speed-8K模型作为本次实验的基座模型。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "chat = ChatCompletion(model=\"ERNIE-Speed-8K\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "```json\n",
      "{\n",
      "\"用户问题\": \"购买的商品一直没有收到，需要查询物流信息。\",\n",
      "\"客服方案\": \"询问用户提供订单号，查询物流信息后告知用户包裹目前正在途中，预计明天送达。\",\n",
      "\"是否接受\": \"用户接受客服提供的方案，表示耐心等待包裹送达。\",\n",
      "\"是否负面情绪\": \"用户没有表现出负面情绪。\"\n",
      "}\n",
      "```\n"
     ]
    }
   ],
   "source": [
    "target ={ \n",
    "    \"conversation\": (\n",
    "        \"用户：你好，我购买的商品一直没有收到，能帮我查一下物流信息吗？\"\n",
    "        \"客服：您好，非常抱歉给您带来不便。请问您能提供一下订单号吗？我帮您查询一下物流信息。\"\n",
    "        \"用户：订单号是123456789，麻烦你了。\"\n",
    "        \"客服：好的，我帮您查询一下。根据物流信息显示，您的包裹目前正在途中，预计明天就能送达，请您耐心等待一下。\"\n",
    "        \"用户：好的，谢谢你。\"\n",
    "        \"客服：不客气，如果后续有任何问题，欢迎您随时联系我们。\"\n",
    ")\n",
    "}\n",
    "prompt = Prompt(\"\"\"你是一个对话摘要机器人，根据下面的已知信息，生成摘要。请使用以下格式输出：{\"用户问题\":xxx\n",
    "\"客服方案\":xxx\n",
    "\"是否接受\":xxx\n",
    "\"是否负面情绪\":xxx}\n",
    "请根据以下会话的内容，概述用户咨询的问题、客服提供的方案、用户是否接受、用户是否有负面情绪：\n",
    "\n",
    "{conversation}\n",
    "\"\"\")\n",
    "\n",
    "\n",
    "\n",
    "resp = chat.do(messages=[{\"role\": \"user\", \"content\": prompt.render(**target)[0]}])\n",
    "\n",
    "print(resp[\"result\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "从上述输出可以看到，大模型的总结能力太差，没有总结出重点信息。并且，结果并不凝练，有很多冗余语句。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 2. Prompt调优示例"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "接下来我们可以进一步去调优我们的 prompt，用prompt工程的方法提升推理效果。\n",
    "\n",
    "通过调用千帆平台提供的prompt优化接口，可先进行初步快速优化：\n",
    "* 平台Prompt优化：https://console.bce.baidu.com/qianfan/prompt/optimize/online \n",
    "* Prompt 优化文档：https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Clommng91 "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "请根据下面的对话内容，总结用户咨询的问题、客服提供的解决方案、用户是否接受了解决方案，以及用户是否有负面情绪。对话内容如下：\n",
      "{conversation}\n",
      "请将以下对话内容进行详细记录：\n",
      "用户：你好，请问有什么可以帮助的吗？\n",
      "客服：您好，请问您遇到了什么问题？\n",
      "用户：我无法登录我的账户，每次都提示我输入错误的密码。\n",
      "客服：很抱歉听到这个问题。您可以再确认一下您的账户名和密码是否正确吗？\n",
      "用户：我确定账户名和密码都没错，可能是我的网络连接有问题。\n",
      "客服：好的，您可以尝试重新启动您的路由器或者联系您的网络服务提供商以寻求帮助。如果问题仍然存在，请随时联系我们。\n",
      "用户：谢谢你的帮助！\n",
      "客服：不用客气，祝您一切顺利！\n",
      "请根据以上对话内容，总结用户咨询的问题、客服提供的解决方案、用户是否接受了解决方案，以及用户是否有负面情绪。\n"
     ]
    }
   ],
   "source": [
    "optimized_prompt = prompt.optimize()\n",
    "print(optimized_prompt.template)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "可以看到，生成效果有所提升，但是仍然存在一些问题，比如：\n",
    "\n",
    "1.完全没有达到概括的效果，只是将对话以叙述的形式重新表述出来。\n",
    "\n",
    "2.在判断用户是否接受解决方案时，给出的答案仍然有很多冗余内容。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "用户咨询的问题：购买的商品一直没有收到。用户遇到了登录账户问题时，账户名和密码无法登录成功。\n",
      "\n",
      "客服提供的解决方案：对于商品未收到的问题，客服查询了订单状态并告知用户商品因物流原因可能会有延迟，会联系物流公司加快配送速度，但无法给出确切的送货时间。对于登录问题，客服建议用户确认账户名和密码是否正确，并尝试重新启动路由器或联系网络服务提供商寻求帮助。\n",
      "\n",
      "用户是否接受了解决方案：对于商品未收到的问题，用户表示理解并希望尽快解决；对于登录问题，用户感谢客服的帮助。从对话内容来看，用户接受了客服的解决方案，并且情绪较为平和，没有明显的负面情绪。\n"
     ]
    }
   ],
   "source": [
    "resp = chat.do(messages=[{\n",
    "    \"role\": \"user\",\n",
    "    \"content\": optimized_prompt.render(\n",
    "        **target\n",
    "    )[0]\n",
    "}])\n",
    "\n",
    "print(resp['result'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. SFT调优示例"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3.1 数据集准备"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在开始进行微调前，我们需要一个优秀的数据集。\n",
    "\n",
    "数据集的收集，处理，拆分可以参考以下文档：[作文批改](https://github.com/baidubce/bce-qianfan-sdk/blob/main/cookbook/awesome_demo/essay_scoring/main.ipynb)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "首先从平台中获取微调用的训练集"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "[INFO][2024-08-06 15:43:19.029] dataset.py:430 [t:8361779456]: no data source was provided, construct\n",
      "[INFO][2024-08-06 15:43:19.030] dataset.py:282 [t:8361779456]: construct a qianfan data source from existed id: ds-1j390abu4fv5abkf, with args: {'format': <FormatType.Jsonl: 'jsonl'>}\n",
      "[INFO][2024-08-06 15:43:19.565] dataset_utils.py:317 [t:8361779456]: list qianfan dataset data by 0\n",
      "[INFO][2024-08-06 15:43:19.966] dataset_utils.py:339 [t:8361779456]: received dataset list from qianfan dataset\n",
      "[INFO][2024-08-06 15:43:19.966] dataset_utils.py:347 [t:8361779456]: retrieve single entity from https://easydata.bj.bcebos.com/_system_/dataset/ds-1j390abu4fv5abkf/texts/data/raw_a804c4af3bec2f18cfeb42c162e2db7b01609497af9b95729b2fa715bebd61ad_b43f5c32f920433bb182ff05ae5c0f02?authorization=bce-auth-v1%2F50c8bb753dcb4e1d8646bb1ffefd3503%2F2024-08-06T07%3A43%3A20Z%2F7200%2Fhost%2Fbb7bdebd741d0392e4378c721d7bf16b45ae1ad99c37b146aab0d940c8252ddb in try 0\n",
      "[INFO][2024-08-06 15:43:20.291] dataset_utils.py:361 [t:8361779456]: retrieve single entity from https://easydata.bj.bcebos.com/_system_/dataset/ds-1j390abu4fv5abkf/texts/data/raw_a804c4af3bec2f18cfeb42c162e2db7b01609497af9b95729b2fa715bebd61ad_b43f5c32f920433bb182ff05ae5c0f02?authorization=bce-auth-v1%2F50c8bb753dcb4e1d8646bb1ffefd3503%2F2024-08-06T07%3A43%3A20Z%2F7200%2Fhost%2Fbb7bdebd741d0392e4378c721d7bf16b45ae1ad99c37b146aab0d940c8252ddb succeeded, with content: [{\"prompt\": \"你是一个对话摘要机器人，根据下面的已知信息，生成摘要。请使用以下格式输出：{\\\"用户问题\\\":xxx\\n\\\"客服方案\\\":xxx\\n\\\"是否接受\\\":xxx\\n\\\"是否负面情绪\\\":xxx}\\n请根据以下会话的内容，概述用户咨询的问题、客服提供的方案、用户是否接受、用户是否有负面情绪：用户：你好，我想退货\\n客服：您好，请问是什么原因需要退货呢？\\n用户：买错了，不想要了\\n客服：好的，您可以在订单页面申请退货退款，我们会在审核通过后为您处理退款事宜\\n用户：好的，我已经申请了，多久能审核通过？\\n客服：通常会在1-3个工作日内审核完毕，请您耐心等待\\n用户：好的，谢谢\\n客服：不客气，请问您还有其他问题需要咨询吗？\\n用户：没有了\", \"response\": [[\"{\\\"用户问题\\\": \\\"买错了商品，需要退货退款并询问审核时间\\\", \\\"客服方案\\\": \\\"指引用户在订单页面申请退货退款，并告知通常会在1-3个工作日内审核完毕\\\", \\\"是否接受\\\": \\\"用户接受客服方案并表示感谢\\\", \\\"是否负面情绪\\\": \\\"用户没有表现出负面情绪\\\"}\"]]}]\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[{'entity_id': 'a804c4af3bec2f18cfeb42c162e2db7b01609497af9b95729b2fa715bebd61ad_b43f5c32f920433bb182ff05ae5c0f02', 'entity_content': '[{\"prompt\": \"你是一个对话摘要机器人，根据下面的已知信息，生成摘要。请使用以下格式输出：{\\\\\"用户问题\\\\\":xxx\\\\n\\\\\"客服方案\\\\\":xxx\\\\n\\\\\"是否接受\\\\\":xxx\\\\n\\\\\"是否负面情绪\\\\\":xxx}\\\\n请根据以下会话的内容，概述用户咨询的问题、客服提供的方案、用户是否接受、用户是否有负面情绪：用户：你好，我想退货\\\\n客服：您好，请问是什么原因需要退货呢？\\\\n用户：买错了，不想要了\\\\n客服：好的，您可以在订单页面申请退货退款，我们会在审核通过后为您处理退款事宜\\\\n用户：好的，我已经申请了，多久能审核通过？\\\\n客服：通常会在1-3个工作日内审核完毕，请您耐心等待\\\\n用户：好的，谢谢\\\\n客服：不客气，请问您还有其他问题需要咨询吗？\\\\n用户：没有了\", \"response\": [[\"{\\\\\"用户问题\\\\\": \\\\\"买错了商品，需要退货退款并询问审核时间\\\\\", \\\\\"客服方案\\\\\": \\\\\"指引用户在订单页面申请退货退款，并告知通常会在1-3个工作日内审核完毕\\\\\", \\\\\"是否接受\\\\\": \\\\\"用户接受客服方案并表示感谢\\\\\", \\\\\"是否负面情绪\\\\\": \\\\\"用户没有表现出负面情绪\\\\\"}\"]]}]'}]\n"
     ]
    }
   ],
   "source": [
    "ds = Dataset.load(qianfan_dataset_id = \"ds-1j390abu4fv5abkf\", format = FormatType.Jsonl)\n",
    "print(ds[0])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3.2 微调训练与测试\n",
    "\n",
    "拿到一个训练场景或者任务后，往往比较难判断参数应该如何调整。一般使用默认的参数值进行训练即可，平台中的默认参数是多次实验的经验结晶。 接下来介绍参数配置中有两个较为关键的参数：\n",
    "\n",
    "* 迭代轮次（Epoch）: 控制训练过程中的迭代轮数。轮数增加代表会使用训练集对模型训练一次。\n",
    "\n",
    "* 学习率（Learning Rate）: 是在梯度下降的过程中更新权重时的超参数，过高会导致模型难以收敛，过低则会导致模型收敛速度过慢，平台已给出默认推荐值，也可根据经验调整。\n",
    "\n",
    "* 序列长度：如果对话数据的长度较短，建议选择短的序列长度，可以提升训练的速度。\n",
    "\n",
    "本次也针对Epoch和Learning Rate进行简要的调参实验，详细实验结果可以看效果评估数据。\n",
    "\n",
    "如果您是模型训练的专家，千帆也提供了训练更多的高级参数供您选择。这里也建议您初期调参时步长可以设定稍大些，因为较小的超参变动对模型效果的影响小，会被随机波动掩盖。\n",
    "\n",
    "针对我们的任务，此处设计了六组sft实验，参数和训练方法配置如下。\n",
    "\n",
    "实验数据如下：\n",
    "| | 实验1 | 实验2 | 实验3 | 实验4  | 实验5 | 实验6 |\n",
    "|-|-|-|-|-|-|-|\n",
    "| 精调方法 | LoRA | LoRA | LoRA | 全量 | 全量 | 全量 |\n",
    "| Epoch | 6 | 3 | 3 | 3 | 6 | 3 |\n",
    "| Learning Rate | 3e-4 | 3e-4 | 6e-4 | 3e-5 | 3e-5 | 4e-5 |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "创建trainer任务"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "trainer5 = LLMFinetune(\n",
    "    name = \"sft5-1\",\n",
    "    train_type=\"ERNIE-Speed-8K\",\n",
    "    train_config=TrainConfig(\n",
    "        epoch=6,\n",
    "        learning_rate=3e-5,\n",
    "        peft_type=PeftType.ALL,\n",
    "    ),\n",
    "    dataset=ds\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "启动训练任务"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "[INFO][2024-08-06 15:44:27.242] base.py:226 [t:8361779456]: trainer subprocess started, pid: 7952\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[None]\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "[INFO][2024-08-06 15:44:27.250] base.py:202 [t:8361779456]: check running log in .qianfan_exec_cache/lgRriWta/2024-08-06.log\n"
     ]
    }
   ],
   "source": [
    "trainer5.start()\n",
    "print(trainer5.result)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "[INFO][2024-08-06 17:49:22.793] dataset.py:430 [t:8361779456]: no data source was provided, construct\n",
      "[INFO][2024-08-06 17:49:22.793] dataset.py:282 [t:8361779456]: construct a qianfan data source from existed id: ds-1j390abu4fv5abkf, with args: {}\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "{'datasets': {'sourceType': 'Platform',\n",
       "  'versions': [{'versionId': 'ds-1j390abu4fv5abkf'}],\n",
       "  'splitRatio': 20},\n",
       " 'task_id': 'task-3zc6i20ru3ce',\n",
       " 'job_id': 'job-fzi9tezdqm9c',\n",
       " 'metrics': {'BLEU-4': '62.20%',\n",
       "  'ROUGE-1': '77.20%',\n",
       "  'ROUGE-2': '64.09%',\n",
       "  'ROUGE-L': '77.97%',\n",
       "  'EDIT-DISTANCE': '43.07',\n",
       "  'EMBEDDING-DISTANCE': '0.03'},\n",
       " 'checkpoints': [],\n",
       " 'model_set_id': 'am-r40e69pvccu6',\n",
       " 'model_id': 'amv-6fafr99pnzu9'}"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "trainer5.output"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "根据实验结果，我们选取实验5的结果用于评估分析。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| | 实验1 | 实验2 | 实验3 | 实验4  | 实验5 | 实验6 |\n",
    "|-|-|-|-|-|-|-|\n",
    "| 精调方法 | LoRA | LoRA | LoRA | 全量 | 全量 | 全量 |\n",
    "| Epoch | 6 | 3 | 3 | 3 | 6 | 3 |\n",
    "| Learning Rate | 3e-4 | 3e-4 | 6e-4 | 3e-5 | 3e-5 | 4e-5 |\n",
    "| loss |  ![t1](./img/t1.png)  |  ![t2](./img/t2.png)  |  ![t3](./img/t3.png)  |  ![t4](./img/t4.png)  |  ![t5](./img/t5.png)  |  ![t6](./img/t6.png)  |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "发布刚才训练的模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[]\n",
      "ds-1j390abu4fv5abkf\n"
     ]
    }
   ],
   "source": [
    "from qianfan.model import Model\n",
    "\n",
    "# 从`version_id`构造模型：\n",
    "m = Model(id='amv-6fafr99pnzu9')\n",
    "m.auto_complete_info()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "部署我们训练的模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from qianfan.model import Service, DeployConfig\n",
    "from qianfan.model.consts import ServiceType\n",
    "from qianfan.resources.console.consts import DeployPoolType\n",
    "\n",
    "sft_svc: Service = m.deploy(DeployConfig(\n",
    "    name=\"cusserv_1\",\n",
    "    endpoint_prefix=\"customer\",\n",
    "    replicas=1,\n",
    "    pool_type=DeployPoolType.PrivateResource,\n",
    "    service_type=ServiceType.Completion,\n",
    "))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在服务发布并且部署成功后，我们便可以使用微调后的模型进行生成效果展示："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 51,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{\"用户问题\": \"购买的商品未收到，请求查询物流信息\", \"客服方案\": \"提供订单号后，客服查询物流信息并告知用户包裹正在途中，预计明天送达\", \"是否接受\": \"是\", \"是否负面情绪\": \"否\"}\n"
     ]
    }
   ],
   "source": [
    "endpoint = \"wf68bahn_espped_ai_svc\"\n",
    "chat=ChatCompletion(endpoint=endpoint)\n",
    "resp = chat.do(messages=[{\"role\": \"user\", \"content\": prompt.render(**target)[0]}])\n",
    "print(resp[\"result\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们可以看到，经过调优后模型的指令遵循效果提升明显。经过SFT后模型基本都能按照prompt要求的格式输出摘要内容，且全量更新效果更加显著。\n",
    "\n",
    "概括能力显著提升的同时，回答一语中的，语言更加凝练，"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "base",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
